{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Semantic Kernel Tool Use Example\n",
    "\n",
    "This document provides an overview and explanation of the code used to create a Semantic Kernel-based tool that integrates with Azure AI Search for Retrieval-Augmented Generation (RAG). The example demonstrates how to build an AI agent that retrieves travel documents from an Azure AI Search index, augments user queries with semantic search results, and streams detailed travel recommendations."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Initializing the Environment"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Importing Packages\n",
    "The following code imports the necessary packages:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "from azure.core.credentials import AzureKeyCredential\n",
    "from azure.search.documents import SearchClient\n",
    "from azure.search.documents.indexes import SearchIndexClient\n",
    "from azure.search.documents.indexes.models import SearchIndex, SimpleField, SearchFieldDataType, SearchableField\n",
    "\n",
    "from openai import AsyncOpenAI\n",
    "\n",
    "from semantic_kernel.kernel import Kernel\n",
    "from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion\n",
    "from semantic_kernel.contents.chat_history import ChatHistory\n",
    "from semantic_kernel.functions import kernel_function\n",
    "from semantic_kernel.functions.kernel_arguments import KernelArguments\n",
    "from semantic_kernel.connectors.ai import FunctionChoiceBehavior\n",
    "from semantic_kernel.contents.function_call_content import FunctionCallContent\n",
    "from semantic_kernel.contents.function_result_content import FunctionResultContent\n",
    "from semantic_kernel.agents import ChatCompletionAgent\n",
    "\n",
    "from typing import Annotated"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Creating the Semantic Kernel and AI Service\n",
    "\n",
    "A Semantic Kernel instance is created and configured with an asynchronous OpenAI chat completion service. The service is added to the kernel for use in generating responses."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Initialize the asynchronous OpenAI client\n",
    "client = AsyncOpenAI(\n",
    "    api_key=os.environ[\"GITHUB_TOKEN\"],\n",
    "    base_url=\"https://models.inference.ai.azure.com/\"\n",
    ")\n",
    "\n",
    "# Create a Semantic Kernel instance and add an OpenAI chat completion service.\n",
    "kernel = Kernel()\n",
    "chat_completion_service = OpenAIChatCompletion(\n",
    "    ai_model_id=\"gpt-4o-mini\",\n",
    "    async_client=client,\n",
    "    service_id=\"agent\",\n",
    ")\n",
    "kernel.add_service(chat_completion_service)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Defining the Prompt Plugin\n",
    "\n",
    "The PromptPlugin is a native plugin that defines a function to build an augmented prompt using retrieval context"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "KernelPlugin(name='promptPlugin', description=None, functions={})"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "class PromptPlugin:\n",
    "    @kernel_function(\n",
    "        name=\"build_augmented_prompt\",\n",
    "        description=\"Build an augmented prompt using retrieval context or function results.\",\n",
    "    )\n",
    "    @staticmethod\n",
    "    def build_augmented_prompt(query: str, retrieval_context: str) -> str:\n",
    "        return (\n",
    "            f\"Retrieved Context:\\n{retrieval_context}\\n\\n\"\n",
    "            f\"User Query: {query}\\n\\n\"\n",
    "            \"First review the retrieved context, if this does not answer the query, try calling an available plugin functions that might give you an answer. If no context is available, say so.\"\n",
    "        )\n",
    "\n",
    "# Register the plugin with the kernel.\n",
    "kernel.add_plugin(PromptPlugin(), plugin_name=\"promptPlugin\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "KernelPlugin(name='weatherplugin', description=None, functions={'get_destination_temperature': KernelFunctionFromMethod(metadata=KernelFunctionMetadata(name='get_destination_temperature', plugin_name='weatherplugin', description='Get the average temperature for a specific travel destination.', parameters=[KernelParameterMetadata(name='destination', description=None, default_value=None, type_='str', is_required=True, type_object=<class 'str'>, schema_data={'type': 'string'}, include_in_function_choices=True)], is_prompt=False, is_asynchronous=False, return_parameter=KernelParameterMetadata(name='return', description='Returns the average temperature for the destination.', default_value=None, type_='str', is_required=True, type_object=<class 'str'>, schema_data={'type': 'string', 'description': 'Returns the average temperature for the destination.'}, include_in_function_choices=True), additional_properties={}), invocation_duration_histogram=<opentelemetry.metrics._internal.instrument._ProxyHistogram object at 0x000001A3D70A83E0>, streaming_duration_histogram=<opentelemetry.metrics._internal.instrument._ProxyHistogram object at 0x000001A3D72048C0>, method=<bound method WeatherInfoPlugin.get_destination_temperature of <__main__.WeatherInfoPlugin object at 0x000001A3D70F4E90>>, stream_method=None)})"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "class WeatherInfoPlugin:\n",
    "    \"\"\"A Plugin that provides the average temperature for a travel destination.\"\"\"\n",
    "\n",
    "    def __init__(self):\n",
    "        # Dictionary of destinations and their average temperatures\n",
    "        self.destination_temperatures = {\n",
    "            \"maldives\": \"82°F (28°C)\",\n",
    "            \"swiss alps\": \"45°F (7°C)\",\n",
    "            \"african safaris\": \"75°F (24°C)\"\n",
    "        }\n",
    "\n",
    "    @kernel_function(description=\"Get the average temperature for a specific travel destination.\")\n",
    "    def get_destination_temperature(self, destination: str) -> Annotated[str, \"Returns the average temperature for the destination.\"]:\n",
    "        \"\"\"Get the average temperature for a travel destination.\"\"\"\n",
    "        # Normalize the input destination (lowercase)\n",
    "        normalized_destination = destination.lower()\n",
    "\n",
    "        # Look up the temperature for the destination\n",
    "        if normalized_destination in self.destination_temperatures:\n",
    "            return f\"The average temperature in {destination} is {self.destination_temperatures[normalized_destination]}.\"\n",
    "        else:\n",
    "            return f\"Sorry, I don't have temperature information for {destination}. Available destinations are: Maldives, Swiss Alps, and African safaris.\"\n",
    "        \n",
    "kernel.add_plugin(WeatherInfoPlugin(), plugin_name=\"weatherplugin\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Vector Database Initialization\n",
    "\n",
    "We initialize Azure AI Search with persistent storage and add enhanced sample documents. Azure AI Search will be used to store and retrieve documents that provide context for generating accurate responses."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Index 'travel-documents' already exists, using the existing index.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[<azure.search.documents._generated.models._models_py3.IndexingResult at 0x1a3d8bf6ff0>,\n",
       " <azure.search.documents._generated.models._models_py3.IndexingResult at 0x1a3d8ce4ad0>,\n",
       " <azure.search.documents._generated.models._models_py3.IndexingResult at 0x1a3d8ce4b60>,\n",
       " <azure.search.documents._generated.models._models_py3.IndexingResult at 0x1a3d8ce4b30>,\n",
       " <azure.search.documents._generated.models._models_py3.IndexingResult at 0x1a3d8ce4b00>]"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Initialize Azure AI Search with persistent storage\n",
    "search_service_endpoint = os.getenv(\"AZURE_SEARCH_SERVICE_ENDPOINT\")\n",
    "search_api_key = os.getenv(\"AZURE_SEARCH_API_KEY\")\n",
    "index_name = \"travel-documents\"\n",
    "\n",
    "search_client = SearchClient(\n",
    "    endpoint=search_service_endpoint,\n",
    "    index_name=index_name,\n",
    "    credential=AzureKeyCredential(search_api_key)\n",
    ")\n",
    "\n",
    "index_client = SearchIndexClient(\n",
    "    endpoint=search_service_endpoint,\n",
    "    credential=AzureKeyCredential(search_api_key)\n",
    ")\n",
    "\n",
    "# Define the index schema\n",
    "fields = [\n",
    "    SimpleField(name=\"id\", type=SearchFieldDataType.String, key=True),\n",
    "    SearchableField(name=\"content\", type=SearchFieldDataType.String)\n",
    "]\n",
    "\n",
    "index = SearchIndex(name=index_name, fields=fields)\n",
    "\n",
    "# Check if index already exists if not, create it\n",
    "try:\n",
    "    existing_index = index_client.get_index(index_name)\n",
    "    print(f\"Index '{index_name}' already exists, using the existing index.\")\n",
    "except Exception as e:\n",
    "    # Create the index if it doesn't exist\n",
    "    print(f\"Creating new index '{index_name}'...\")\n",
    "    index_client.create_index(index)\n",
    "\n",
    "\n",
    "# Enhanced sample documents\n",
    "documents = [\n",
    "    {\"id\": \"1\", \"content\": \"Contoso Travel offers luxury vacation packages to exotic destinations worldwide.\"},\n",
    "    {\"id\": \"2\", \"content\": \"Our premium travel services include personalized itinerary planning and 24/7 concierge support.\"},\n",
    "    {\"id\": \"3\", \"content\": \"Contoso's travel insurance covers medical emergencies, trip cancellations, and lost baggage.\"},\n",
    "    {\"id\": \"4\", \"content\": \"Popular destinations include the Maldives, Swiss Alps, and African safaris.\"},\n",
    "    {\"id\": \"5\", \"content\": \"Contoso Travel provides exclusive access to boutique hotels and private guided tours.\"}\n",
    "]\n",
    "\n",
    "# Add documents to the index\n",
    "search_client.upload_documents(documents)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A helper function `get_retrieval_context` is defined to query the index and return the top two relevant documents based on the user query:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_retrieval_context(query: str) -> str:\n",
    "    results = search_client.search(query)\n",
    "    context_strings = []\n",
    "    for result in results:\n",
    "        context_strings.append(f\"Document: {result['content']}\")\n",
    "    return \"\\n\\n\".join(context_strings) if context_strings else \"No results found\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Setting the Function Choice Behavior \n",
    "\n",
    "In Semantic Kernel, we have the ability to have some control of the agent choice of functions. This is done by using the `FunctionChoiceBehavior` class. \n",
    "\n",
    "The code below sets it to `Auto` which allows the agent to choose among the available functions or not choose any. \n",
    "\n",
    "This can also be set to:\n",
    "`FunctionChoiceBehavior.Required` - to require the agent to choose at least one function \n",
    "`FunctionChoiceBehavior.NoneInvoke` - instructs the agent to not choose any function. (good for testing)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "settings = kernel.get_prompt_execution_settings_from_service_id(\"agent\")\n",
    "settings.function_choice_behavior = FunctionChoiceBehavior.Auto()\n",
    "arguments = KernelArguments(settings=settings)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "AGENT_NAME = \"TravelAgent\"\n",
    "AGENT_INSTRUCTIONS = (\n",
    "    \"Answer travel queries using the provided tools and context. If context is provided, do not say 'I have no context for that.'\"\n",
    "\n",
    ")\n",
    "agent = ChatCompletionAgent(\n",
    "    kernel=kernel,\n",
    "    name=AGENT_NAME,\n",
    "    instructions=AGENT_INSTRUCTIONS,\n",
    "    arguments=arguments,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A helper function `get_augmented_prompt` forces a call to the plugin to build the augmented prompt. It directly calls the static plugin method:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "async def get_augmented_prompt(query: str) -> str:\n",
    "    retrieval_context = get_retrieval_context(query)\n",
    "    return PromptPlugin.build_augmented_prompt(query, retrieval_context)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Running the Agent with Streaming Chat History\n",
    "The main asynchronous loop creates a chat history for the conversation and, for each user input, first adds the augmented prompt (as a system message) to the chat history so that the agent sees the retrieval context. The user message is also added, and then the agent is invoked using streaming. The output is printed as it streams in."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div style='margin-bottom:10px'><details><summary style='cursor:pointer; font-weight:bold; color:#0066cc;'>RAG Context (click to expand)</summary><div style='margin:10px; padding:10px; background-color:#f8f8f8; border:1px solid #ddd; border-radius:4px; white-space:pre-wrap;'>Retrieved Context:\n",
       "Document: Contoso's travel insurance covers medical emergencies, trip cancellations, and lost baggage.\n",
       "\n",
       "Document: Our premium travel services include personalized itinerary planning and 24/7 concierge support.\n",
       "\n",
       "Document: Contoso Travel provides exclusive access to boutique hotels and private guided tours.\n",
       "\n",
       "Document: Contoso Travel offers luxury vacation packages to exotic destinations worldwide.\n",
       "\n",
       "User Query: Can you explain Contoso's travel insurance coverage?\n",
       "\n",
       "First review the retrieved context, if this does not answer the query, try calling an available plugin functions that might give you an answer. If no context is available, say so.</div></details></div><div style='margin-bottom:10px'><div style='font-weight:bold'>User:</div><div style='margin-left:20px'>Can you explain Contoso's travel insurance coverage?</div></div><div style='margin-bottom:20px'><div style='font-weight:bold'>TravelAgent:</div><div style='margin-left:20px; white-space:pre-wrap'>Contoso's travel insurance coverage includes the following:\n",
       "\n",
       "1. **Medical Emergencies** - Coverage for medical expenses incurred due to emergencies while traveling.\n",
       "2. **Trip Cancellations** - Protection against financial losses if a trip must be canceled for covered reasons.\n",
       "3. **Lost Baggage** - Compensation for baggage that is lost during travel.\n",
       "\n",
       "If you have any specific questions about the coverage details, feel free to ask!</div></div><hr>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div style='margin-bottom:10px'><details><summary style='cursor:pointer; font-weight:bold; color:#0066cc;'>RAG Context (click to expand)</summary><div style='margin:10px; padding:10px; background-color:#f8f8f8; border:1px solid #ddd; border-radius:4px; white-space:pre-wrap;'>Retrieved Context:\n",
       "Document: Popular destinations include the Maldives, Swiss Alps, and African safaris.\n",
       "\n",
       "User Query: What is the average temperature of the Maldives?\n",
       "\n",
       "First review the retrieved context, if this does not answer the query, try calling an available plugin functions that might give you an answer. If no context is available, say so.</div></details></div><div style='margin-bottom:10px'><div style='font-weight:bold'>User:</div><div style='margin-left:20px'>What is the average temperature of the Maldives?</div></div><div style='margin-bottom:10px'><details><summary style='cursor:pointer; font-weight:bold; color:#0066cc;'>Function Calls (click to expand)</summary><div style='margin:10px; padding:10px; background-color:#f8f8f8; border:1px solid #ddd; border-radius:4px; white-space:pre-wrap;'>Calling: get_destination_temperature()<br>Calling: ({\")<br>Calling: (destination)<br>Calling: (\":\")<br>Calling: (Mal)<br>Calling: (dives)<br>Calling: (\"})<br>Result: The average temperature in Maldives is 82°F (28°C).</div></details></div><div style='margin-bottom:20px'><div style='font-weight:bold'>TravelAgent:</div><div style='margin-left:20px; white-space:pre-wrap'>The average temperature in the Maldives is82°F (28°C). If you have any more questions or need further information, feel free to ask!</div></div><hr>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div style='margin-bottom:10px'><details><summary style='cursor:pointer; font-weight:bold; color:#0066cc;'>RAG Context (click to expand)</summary><div style='margin:10px; padding:10px; background-color:#f8f8f8; border:1px solid #ddd; border-radius:4px; white-space:pre-wrap;'>Retrieved Context:\n",
       "Document: Contoso Travel provides exclusive access to boutique hotels and private guided tours.\n",
       "\n",
       "Document: Contoso Travel offers luxury vacation packages to exotic destinations worldwide.\n",
       "\n",
       "Document: Contoso's travel insurance covers medical emergencies, trip cancellations, and lost baggage.\n",
       "\n",
       "Document: Popular destinations include the Maldives, Swiss Alps, and African safaris.\n",
       "\n",
       "Document: Our premium travel services include personalized itinerary planning and 24/7 concierge support.\n",
       "\n",
       "User Query: What is a good cold destination offered by Contoso and what is it average temperature?\n",
       "\n",
       "First review the retrieved context, if this does not answer the query, try calling an available plugin functions that might give you an answer. If no context is available, say so.</div></details></div><div style='margin-bottom:10px'><div style='font-weight:bold'>User:</div><div style='margin-left:20px'>What is a good cold destination offered by Contoso and what is it average temperature?</div></div><div style='margin-bottom:10px'><details><summary style='cursor:pointer; font-weight:bold; color:#0066cc;'>Function Calls (click to expand)</summary><div style='margin:10px; padding:10px; background-color:#f8f8f8; border:1px solid #ddd; border-radius:4px; white-space:pre-wrap;'>Calling: get_destination_temperature()<br>Calling: ({\"de)<br>Calling: (stina)<br>Calling: (tion\":)<br>Calling: ( \"Sw)<br>Calling: (iss A)<br>Calling: (lps\"})<br>Calling: get_destination_temperature()<br>Calling: ({\"de)<br>Calling: (stina)<br>Calling: (tion\":)<br>Calling: ( \"Af)<br>Calling: (rican)<br>Calling: ( safar)<br>Calling: (is\"})<br>Result: The average temperature in Swiss Alps is 45°F (7°C).<br>Result: The average temperature in African safaris is 75°F (24°C).</div></details></div><div style='margin-bottom:20px'><div style='font-weight:bold'>TravelAgent:</div><div style='margin-left:20px; white-space:pre-wrap'>A good cold destination offered by Contoso Travel is the **Swiss Alps**, where the average temperature is45°F (7°C). If you have more questions or need assistance with anything else, feel free to ask!</div></div><hr>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import display, HTML\n",
    "\n",
    "\n",
    "async def main():\n",
    "    # Create a chat history.\n",
    "    chat_history = ChatHistory()\n",
    "\n",
    "    user_inputs = [\n",
    "        # Retrieval context available.\n",
    "        \"Can you explain Contoso's travel insurance coverage?\",\n",
    "        \"What is the average temperature of the Maldives?\",\n",
    "        \"What is a good cold destination offered by Contoso and what is it average temperature?\"\n",
    "        # \"What is Neural Network?\"  # No retrieval context available.\n",
    "    ]\n",
    "\n",
    "    for user_input in user_inputs:\n",
    "        # Add the user message to chat history\n",
    "        chat_history.add_user_message(user_input)\n",
    "        augmented_prompt = await get_augmented_prompt(user_input)\n",
    "        \n",
    "        chat_history.add_system_message(\n",
    "            f\"Here is relevant information to help answer the user's question: {augmented_prompt}\")\n",
    "\n",
    "\n",
    "        # Display the augmented prompt in a collapsible section\n",
    "        html_output = f\"<div style='margin-bottom:10px'>\"\n",
    "        html_output += f\"<details>\"\n",
    "        html_output += f\"<summary style='cursor:pointer; font-weight:bold; color:#0066cc;'>RAG Context (click to expand)</summary>\"\n",
    "        html_output += f\"<div style='margin:10px; padding:10px; background-color:#f8f8f8; border:1px solid #ddd; border-radius:4px; white-space:pre-wrap;'>{augmented_prompt}</div>\"\n",
    "\n",
    "        html_output += f\"</details>\"\n",
    "        html_output += f\"</div>\"\n",
    "\n",
    "        # Show user query\n",
    "        html_output += f\"<div style='margin-bottom:10px'>\"\n",
    "        html_output += f\"<div style='font-weight:bold'>User:</div>\"\n",
    "        html_output += f\"<div style='margin-left:20px'>{user_input}</div>\"\n",
    "        html_output += f\"</div>\"\n",
    "\n",
    "        agent_name: str | None = None\n",
    "        full_response = \"\"\n",
    "        function_calls = []\n",
    "        function_results = {}\n",
    "\n",
    "        # Collect the agent's response with improved content handling\n",
    "        async for content in agent.invoke_stream(chat_history):\n",
    "            if not agent_name and hasattr(content, 'name'):\n",
    "                agent_name = content.name\n",
    "\n",
    "            # Track function calls and results\n",
    "            for item in content.items:\n",
    "                if isinstance(item, FunctionCallContent):\n",
    "                    call_info = f\"Calling: {item.function_name}({item.arguments})\"\n",
    "                    function_calls.append(call_info)\n",
    "                elif isinstance(item, FunctionResultContent):\n",
    "                    result_info = f\"Result: {item.result}\"\n",
    "                    function_calls.append(result_info)\n",
    "                    # Store function results to possibly add to chat history\n",
    "                    function_results[item.function_name] = item.result\n",
    "\n",
    "            # Better content extraction - make sure we're getting the actual text\n",
    "            if hasattr(content, 'content') and content.content and content.content.strip():\n",
    "                # Check if this is a regular text message (not function related)\n",
    "                if not any(isinstance(item, (FunctionCallContent, FunctionResultContent))\n",
    "                           for item in content.items):\n",
    "                    full_response += content.content\n",
    "\n",
    "        # Add function call info to chat history\n",
    "        if function_results:\n",
    "            # Even if we have some response text, we want to make sure function results are incorporated\n",
    "            function_results_message = \"Function calls completed with the following results: \" + \\\n",
    "                str(function_results)\n",
    "            chat_history.add_system_message(function_results_message)\n",
    "\n",
    "            # Get final response from agent that incorporates the function results\n",
    "            collected_response = \"\"\n",
    "            async for content in agent.invoke_stream(chat_history):\n",
    "                if hasattr(content, 'content') and content.content and content.content.strip():\n",
    "                    collected_response += content.content\n",
    "\n",
    "            if collected_response:\n",
    "                full_response = collected_response\n",
    "\n",
    "        # Add function calls to HTML if any occurred\n",
    "        if function_calls:\n",
    "            html_output += f\"<div style='margin-bottom:10px'>\"\n",
    "            html_output += f\"<details>\"\n",
    "            html_output += f\"<summary style='cursor:pointer; font-weight:bold; color:#0066cc;'>Function Calls (click to expand)</summary>\"\n",
    "            html_output += f\"<div style='margin:10px; padding:10px; background-color:#f8f8f8; border:1px solid #ddd; border-radius:4px; white-space:pre-wrap;'>\"\n",
    "            html_output += \"<br>\".join(function_calls)\n",
    "            html_output += f\"</div></details></div>\"\n",
    "\n",
    "        # Add agent response to HTML - make sure we have a valid response\n",
    "        html_output += f\"<div style='margin-bottom:20px'>\"\n",
    "        html_output += f\"<div style='font-weight:bold'>{agent_name or 'Assistant'}:</div>\"\n",
    "        html_output += f\"<div style='margin-left:20px; white-space:pre-wrap'>{full_response}</div>\"\n",
    "        html_output += f\"</div>\"\n",
    "        html_output += \"<hr>\"\n",
    "\n",
    "        # Add agent's response to chat history\n",
    "        if full_response:\n",
    "            chat_history.add_assistant_message(full_response)\n",
    "\n",
    "        # Display formatted HTML\n",
    "        display(HTML(html_output))\n",
    "\n",
    "await main()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}